2 // AIImageViewWithImagePicker.m
5 // Created by Evan Schoenberg on Sun Jun 06 2004.
6 // Copyright (c) 2004-2005 The Adium Team. All rights reserved.
9 #import "AITigerCompatibility.h"
11 #import "AIImageViewWithImagePicker.h"
12 #import <Quartz/Quartz.h>
14 #import "AIImageAdditions.h"
15 #import "AIFileManagerAdditions.h"
16 #import "AIApplicationAdditions.h"
17 #import "AIStringUtilities.h"
18 #import "IKPictureTakerForTiger.h"
20 #define DRAGGING_THRESHOLD 16.0
22 @interface AIImageViewWithImagePicker (PRIVATE)
23 - (void)_initImageViewWithImagePicker;
24 - (void)showPictureTaker;
25 - (void)copy:(id)sender;
26 - (void)paste:(id)sender;
31 #define IKPictureTakerClass ([NSApp isOnLeopardOrBetter] ? NSClassFromString(@"IKPictureTaker") : NSClassFromString(@"IKPictureTakerForTiger"))
34 * @class AIImageViewWithImagePicker
36 * @brief Image view which displays and uses the Image Picker used by Apple Address Book and iChat when activated and also allows other image-setting behaviors.
38 * The following is supported
39 * - Address book-style image picker on double-click or enter, with delegate notification
40 * - Or, alternately, an Open Panel on double-click or enter, with delegate notification
41 * - Copying and pasting, with delegate notification
42 * - Drag and drop into and out of the image well, with delegate notification,
43 * with support for animated GIFs and transparency
44 * - Notifcation to the delegate of user's attempt to delete the image
46 * Note: AIImageViewWithImagePicker requires Panther or better for the Address Book-style
47 * image picker to work.
49 @implementation AIImageViewWithImagePicker
51 // Init ------------------------------------------------------------------------------------------
54 * @brief Initialize with coder
56 - (id)initWithCoder:(NSCoder *)aDecoder
58 if ((self = [super initWithCoder:aDecoder])) {
59 [self _initImageViewWithImagePicker];
65 * @brief Initialize with frame
67 - (id)initWithFrame:(NSRect)frameRect
69 if ((self = [super initWithFrame:frameRect])) {
70 [self _initImageViewWithImagePicker];
76 * @brief Private initialization method
78 - (void)_initImageViewWithImagePicker
85 shouldDrawFocusRing = NO;
87 mouseDownPos = NSZeroPoint;
90 usePictureTaker = YES;
100 [pictureTaker release]; pictureTaker = nil;
109 // Getters and Setters ----------------------------------------------------------------
110 #pragma mark Getters and Setters
112 * @brief Set the delegate
114 * Set the delegate. See <tt>AIImageViewWithImagePickerDelegate</tt> protocol discussion for details.
115 * @param inDelegate The delegate, which may implement any of the methods described in <tt>AIImageViewWithImagePickerDelegate</tt>.
117 - (void)setDelegate:(id)inDelegate
119 delegate = inDelegate;
123 * @brief Return the delegate
125 * @return The delegate
133 * @brief Set the image
135 * We may get here progrmatically, from a user drag-and-drop or paste, etc.
137 - (void)setImage:(NSImage *)inImage
139 [super setImage:inImage];
141 //Inform the picker controller of a changed selection if it is open, for live updating
143 [pictureTaker setInputImage:inImage];
148 * @brief Set the title of the Image Picker
150 * Set the title of the Image Picker window which will be displayed if the user activates it (see class discussion).
151 * @param inTitle An <tt>NSString</tt> of the title
153 - (void)setTitle:(NSString *)inTitle
155 if (title != inTitle) {
156 [title release]; title = [inTitle retain];
158 [pictureTaker setTitle:title];
164 * @brief The title of the image picker
168 return (title ? title : AILocalizedStringFromTableInBundle(@"Image Picker", nil, [NSBundle bundleWithIdentifier:AIUTILITIES_BUNDLE_ID], nil));
172 * @brief Should the image view use the address book Image Picker?
174 * If NO, a standard Open panel is used instead.
176 - (void)setUsePictureTaker:(BOOL)inUsePictureTaker
178 usePictureTaker = inUsePictureTaker;
181 - (void)setPresentPictureTakerAsSheet:(BOOL)inPresentPictureTakerAsSheet
183 presentPictureTakerAsSheet = inPresentPictureTakerAsSheet;
186 - (BOOL)presentPictureTakerAsSheet
188 return presentPictureTakerAsSheet;
191 - (void)setMaxSize:(NSSize)inMaxSize
201 // Monitoring user interaction --------------------------------------------------------
202 #pragma mark Monitoring user interaction
207 * Intercept mouse down events so we can begin a drag out of the image view if appropriate
209 - (void)mouseDown:(NSEvent *)theEvent
211 if ([self isEnabled]) {
214 //Wait for the next event
215 nextEvent = [[self window] nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSPeriodicMask)
216 untilDate:[NSDate distantFuture]
217 inMode:NSEventTrackingRunLoopMode
220 mouseDownPos = [self convertPoint:[theEvent locationInWindow] fromView:nil];
222 /* If the user starts dragging, don't call mouse down as we won't receive mouse dragged events, as it seems that
223 * NSImageView does some sort of event loop modification in response to a click. We didn't dequeue the event, so
224 * we don't have to handle it ourselves -- instead, the event loop will handle it after this invocation is complete.
226 if ([nextEvent type] != NSLeftMouseDragged) {
227 [super mouseDown:theEvent];
230 if ([theEvent clickCount] == 2) {
231 [self showPictureTaker];
235 [super mouseDown:theEvent];
242 * Intercept key down events to delete the image on delete/backspace or to show the image picker on enter/return
244 - (void)keyDown:(NSEvent *)theEvent
246 NSString *characters = [theEvent charactersIgnoringModifiers];
247 unichar key = ([characters length] ? [characters characterAtIndex:0] : 0);
249 if ((key == NSBackspaceCharacter) || (key == NSDeleteCharacter) || (key == NSDeleteFunctionKey) || (key == NSDeleteCharFunctionKey)) {
251 } else if (key == NSEnterCharacter || key == NSCarriageReturnCharacter) {
252 [self showPictureTaker];
254 [super keyDown:theEvent];
259 * @brief Mouse dragged
261 * Begin an image drag as appropriate
263 - (void)mouseDragged:(NSEvent *)theEvent
265 if (![self image]) return;
267 // Work out if the mouse has been dragged far enough - it stops accidental drags
268 NSPoint mousePos = [self convertPoint:[theEvent locationInWindow] fromView:nil];
269 float dx = mousePos.x-mouseDownPos.x;
270 float dy = mousePos.y-mouseDownPos.y;
271 if ((dx*dx) + (dy*dy) < DRAGGING_THRESHOLD) {
276 [self dragPromisedFilesOfTypes:[NSArray arrayWithObject:@"png"]
283 - (void)dragImage:(NSImage *)anImage at:(NSPoint)imageLoc offset:(NSSize)mouseOffset event:(NSEvent *)theEvent pasteboard:(NSPasteboard *)pboard source:(id)sourceObject slideBack:(BOOL)slideBack
285 [pboard addTypes:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,nil] owner:self];
287 NSImage *dragImage = [[NSImage alloc] initWithSize:[[self image] size]];
289 //Draw our original image as 50% transparent
290 [dragImage lockFocus];
291 [[self image] dissolveToPoint:NSZeroPoint fraction:0.5];
292 [dragImage unlockFocus];
294 //We want the image to resize
295 [dragImage setScalesWhenResized:YES];
296 //Change to the size we are displaying
297 [dragImage setSize:[self bounds].size];
299 [super dragImage:dragImage
305 slideBack:slideBack];
310 * @brief Declare what operations we can participate in as a drag and drop source
312 - (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)flag
314 return NSDragOperationCopy;
318 * @brief Method called to support drag types we said we could offer
320 - (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type
322 //sender has accepted the drag and now we need to send the data for the type we promised
323 if ([type isEqualToString:NSTIFFPboardType]) {
324 //set data for TIFF type on the pasteboard as requested
325 [sender setData:[[self image] TIFFRepresentation]
326 forType:NSTIFFPboardType];
328 } else if ([type isEqualToString:NSPDFPboardType]) {
329 [sender setData:[self dataWithPDFInsideRect:[self bounds]]
330 forType:NSPDFPboardType];
335 * @brief Dragging entered
337 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
339 if ([sender draggingSource] == self) {
340 return NSDragOperationNone;
342 return [super draggingEntered:sender];
347 * @brief Dragging updated
349 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
351 if ([sender draggingSource] == self) {
352 return NSDragOperationNone;
354 return [super draggingUpdated:sender];
358 - (NSArray *)namesOfPromisedFilesDroppedAtDestination:(NSURL *)inDropDestination
360 NSString *name = nil;
361 if ([[self delegate] respondsToSelector:@selector(fileNameForImageInImagePicker:)]) {
362 name = [[self delegate] fileNameForImageInImagePicker:self];
363 if (![name length]) name = nil;
367 name = NSLocalizedString(@"Picture", nil);
369 name = [name stringByAppendingPathExtension:@"png"];
371 NSString *fullPath = [[inDropDestination path] stringByAppendingPathComponent:name];
372 fullPath = [[NSFileManager defaultManager] uniquePathForPath:fullPath];
374 [[[self image] PNGRepresentation] writeToFile:fullPath
377 return [NSArray arrayWithObject:[fullPath lastPathComponent]];
381 * @brief Conclude a drag operation
383 * A new image was dragged into our view. -[super concludeDragOperation:] will change [self image] to match it.
384 * We then want to update our pictureTaker's selection if it is open.
385 * Also, if we're dropped a promised file, use its data directly as it may be better than what NSImageView's natural
386 * loading retrieves... this way we can get transparency or animation data, for example.
388 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
390 BOOL notified = NO, resized = NO;
391 NSImage *droppedImage;
392 NSSize droppedImageSize;
394 [super concludeDragOperation:sender];
396 droppedImage = [self image];
397 droppedImageSize = [droppedImage size];
399 if ((maxSize.width > 0 && droppedImageSize.width > maxSize.width) ||
400 (maxSize.height > 0 && droppedImageSize.height > maxSize.height)) {
401 droppedImage = [droppedImage imageByScalingToSize:maxSize];
402 //This will notify the picker controller that the selection changed, as well
403 [self setImage:droppedImage];
406 } else if (pictureTaker) {
407 [pictureTaker setInputImage:droppedImage];
410 //Use the file's data if possible and the image wasn't too big
411 if (!resized && [delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
412 NSPasteboard *pboard = [sender draggingPasteboard];
414 if ([[pboard types] containsObject:NSFilenamesPboardType]) {
415 NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
418 NSString *imageFile = [files objectAtIndex:0];
419 NSData *imageData = [NSData dataWithContentsOfFile:imageFile];
422 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
424 withObject:[NSData dataWithContentsOfFile:imageFile]];
432 //Inform the delegate if we haven't informed it yet
434 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
435 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
437 withObject:[droppedImage PNGRepresentation]];
439 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
440 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
442 withObject:droppedImage];
447 // Copy / Paste ----------------------------------------------------------------
448 #pragma mark Copy / Paste
452 - (void)copy:(id)sender
454 NSImage *image = [self image];
456 [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSTIFFPboardType] owner:nil];
457 [[NSPasteboard generalPasteboard] setData:[image TIFFRepresentation] forType:NSTIFFPboardType];
464 - (void)paste:(id)sender
466 NSPasteboard *pb = [NSPasteboard generalPasteboard];
467 NSString *type = [pb availableTypeFromArray:
468 [NSArray arrayWithObjects:NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType,nil]];
471 NSData *imageData = (type ? [pb dataForType:type] : nil);
473 NSImage *image = [[[NSImage alloc] initWithData:imageData] autorelease];
475 NSSize imageSize = [image size];
477 if ((maxSize.width > 0 && imageSize.width > maxSize.width) ||
478 (maxSize.height > 0 && imageSize.height > maxSize.height)) {
479 image = [image imageByScalingToSize:maxSize];
480 imageData = [image PNGRepresentation];
483 [self setImage:image];
486 [pictureTaker setInputImage:image];
489 //Inform the delegate
491 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
492 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
494 withObject:imageData];
495 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
496 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
506 if (!success) NSBeep();
512 * Cut = copy + delete
514 - (void)cut:(id)sender
525 if (delegate && [delegate respondsToSelector:@selector(deleteInImageViewWithImagePicker:)]) {
526 [delegate performSelector:@selector(deleteInImageViewWithImagePicker:)
531 // NSImagePicker Access and Delegate ----------------------------------------------------------------
532 #pragma mark NSImagePicker Access and Delegate
534 * @brief Action to call -[self showPictureTaker]
536 - (IBAction)showImagePicker:(id)sender
538 [self showPictureTaker];
541 - (void)pictureTakerDidEnd:(id)inPictureTaker returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
543 if (returnCode == NSOKButton) {
544 NSImage *image = [inPictureTaker outputImage];
546 //Update the NSImageView
547 [self setImage:image];
549 //Inform the delegate, but only if NOT using NSOpenPanel
550 if (delegate && usePictureTaker) {
551 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
552 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
554 withObject:[image PNGRepresentation]];
556 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
557 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
566 * @brief Show the image picker controller
568 - (void)showPictureTaker
570 if (usePictureTaker) {
572 pictureTaker = [[IKPictureTakerClass pictureTaker] retain];
573 [pictureTaker setDelegate:self];
574 [pictureTaker setTitle:title];
577 NSImage *theImage = nil;
579 //Give the delegate an opportunity to supply an image which differs from the NSImageView's image
580 if (delegate && [delegate respondsToSelector:@selector(imageForImageViewWithImagePicker:)]) {
581 theImage = [delegate imageForImageViewWithImagePicker:self];
584 [pictureTaker setInputImage:(theImage ? theImage : [self image])];
585 [pictureTaker setValue:[NSValue valueWithSize:[self maxSize]]
586 forKey:IKPictureTakerOutputImageMaxSizeKey];
588 if ([self presentPictureTakerAsSheet]) {
589 [pictureTaker beginPictureTakerSheetForWindow:[self window]
591 didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
594 [pictureTaker beginPictureTakerWithDelegate:self
595 didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
600 /* If we aren't using or can't use the image picker, use an open panel */
601 NSOpenPanel *openPanel;
603 openPanel = [NSOpenPanel openPanel];
604 [openPanel setTitle:[NSString stringWithFormat:AILocalizedStringFromTableInBundle(@"Select Image", nil, [NSBundle bundleWithIdentifier:AIUTILITIES_BUNDLE_ID], nil)]];
606 if ([openPanel runModalForDirectory:nil file:nil types:[NSImage imageFileTypes]] == NSOKButton) {
611 imageData = [NSData dataWithContentsOfFile:[openPanel filename]];
612 image = (imageData ? [[[NSImage alloc] initWithData:imageData] autorelease] : nil);
613 imageSize = (image ? [image size] : NSZeroSize);
615 if ((maxSize.width > 0 && imageSize.width > maxSize.width) ||
616 (maxSize.height > 0 && imageSize.height > maxSize.height)) {
617 image = [image imageByScalingToSize:maxSize];
618 imageData = [image PNGRepresentation];
621 //Update the image view
622 [self setImage:image];
624 //Inform the delegate
626 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
627 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
629 withObject:imageData];
631 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
632 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
641 // Drawing ------------------------------------------------------------------------
644 * @brief Note when the focus ring needs to be displayed
646 * Focus ring drawing code by Nicholas Riley, posted unlicensed as public domain on cocoadev and available at:
647 * http://cocoa.mamasam.com/COCOADEV/2002/03/2/29535.php
651 NSResponder *resp = nil;
652 NSWindow *window = [self window];
654 if ([window isKeyWindow]) {
655 resp = [window firstResponder];
656 if (resp == lastResp) {
657 return [super needsDisplay];
660 } else if (lastResp == nil) {
661 return [super needsDisplay];
665 shouldDrawFocusRing = (resp != nil &&
666 [resp isKindOfClass:[NSView class]] &&
667 [(NSView *)resp isDescendantOf:self]); // [sic]
670 [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]];
675 * @brief Draw the focus ring around our view if necessary
677 - (void)drawRect:(NSRect)rect
679 [super drawRect:rect];
681 if (shouldDrawFocusRing) {
682 NSSetFocusRingStyle(NSFocusRingOnly);